一 本文目的

  • 学习 HTTP2 协议.
  • 学习 NGINX 对 HTTP2 的处理以及如何从 HTTP2 进入 HTTP1.1 的处理流程.
  • 未分析 HEADERS、DATA 之外帧处理.

二 协议概述

HTTP2 可以同时运行在 HTTP/HTTPS 之上, 在 HTTPS 上是通过 TLS 应用层协商协议(Application-Layer Protocol Negotiation 简称 ALPN)支持. NGINX 服务器是支持在 HTTP 下开启 HTTP2, 但是无法在同一个端口上同时支持 HTTP2、HTTP. 浏览器厂商选择只实现基于 HTTPS 的 HTTP2, 使用 ALPN 可以判断使用 HTTP/HTTP2.

HTTP2 协议特性:

  • 二进制分帧协议

  • 无队头堵塞: 可以并行发送多个 HTTP 请求, 不会触发队头阻塞.

  • 多路复用

    二进制流可以交错进行, 实现在单 TCP 连接上多路复用. 同时连接复用有效避免 TCP 慢启动、拥塞窗口协商过程, 提升传输效率. 可以将 Stream 想象为一系列 Frame 序列. 参见 Streams And Multiplexing.

  • 服务端推送

  • 头部压缩

    头部压缩是有状态的, 在一个连接上只有一个压缩、解压上下文.

NGINX 默认没有编译 HTTP2 模块, 需要通过 --with-http_v2_module 选项开启.

协议细节参考 RFC 文档. 在规范中未定义 Frame ID 类似字段, 是通过 TCP 协议来确保同一个 Stream 中的 Frame 是有序的.

1. 交互顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram

participant C as Client
participant S as Server

C --> S : Idle
C ->> S : HTTP2 前言, PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
C ->> S : SETTINGS
S ->> C : SETTINGS, HTTP2 连接建立成功
C --> S : HEADERS
C --> S : DATA
C --> S : ...


协议规定, 对端响应的 SETTINGS 帧是前言的一部分, 因此 Server 必须发送 SETTINGS 帧.

对基于 HTTPS 的 HTTP2 在 SSL 握手阶段需要进行应用层协议协商(ALPN), 当 Sever 允许使用 HTTP2 时会响应 101 状态码进行协议切换, 会产生一次额外的 RTT.

Stream ID 约束: 客户端使用奇数流标识符; 服务端使用偶数流标识符. 特殊的 0 用于整个连接而非单独的流. 可以观察到 SETTINGS 帧的 Stream ID 都是 0.

流中可以有多个帧, 会出现多个帧具有相同的 Stream ID, 这些帧不会错乱是因为: 1. 发送方在同一个流中顺序发送; 2. TCP 协议确保帧会按发送顺序交付.

三 NGINX 中处理

对于 H2 处理有个比较重要的问题, 如何对一个 TCP 链接进行多路复用. NGINX 使用 fake_connection 与 H2 的 Stream 关联, 将其等价于 HTTP1.1 的 request 复用原有模块.

1. H2 入口

NGINX 中 HTTP2 协议处理有两个入口: 基于 HTTP 和基于 HTTPS.

  • 当配置 http2 指令, 未开启 HTTPS 时, 会在 ngx_http_init_connection 阶段修改当前连接的接收处理函数为 ngx_http_v2_init 陷入 HTTP2 协议处理流程.

  • 当开启 HTTPS 时, 会在 SSL 握手阶段, 根据请求协商信息确定是否启用 HTTP2.

2. H2 处理开始

NGINX 中 HTTP2 协议是由 ‘ngx_http_v2.c’ 模块实现.

在与客户端交互过程中, NGINX 会首先发送一个 SETTINGS 和 WINDOW_UPDATE 帧, 通知客户端 NGINX 支持的最大流数量、窗口大小、帧大小, 此时帧会先进行 queue 合并发送. TCP 连接的读/写事件处理函数为 ngx_http_v2_read_handler, ngx_http_v2_write_handler, HTTP2 状态机初始状态是 ngx_http_v2_state_preface.

ngx_http_v2_state_preface 检查发送“前言”是否正确, “前言”必须是 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n. 在“前言”处理完毕后会进入请求头处理 ngx_http_v2_state_head, 此时会根据帧首部 Type 使用不同的处理函数处理. 不同类型帧对应处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
static ngx_http_v2_handler_pt ngx_http_v2_frame_states[] = {
ngx_http_v2_state_data, /* NGX_HTTP_V2_DATA_FRAME */
ngx_http_v2_state_headers, /* NGX_HTTP_V2_HEADERS_FRAME */
ngx_http_v2_state_priority, /* NGX_HTTP_V2_PRIORITY_FRAME */
ngx_http_v2_state_rst_stream, /* NGX_HTTP_V2_RST_STREAM_FRAME */
ngx_http_v2_state_settings, /* NGX_HTTP_V2_SETTINGS_FRAME */
ngx_http_v2_state_push_promise, /* NGX_HTTP_V2_PUSH_PROMISE_FRAME */
ngx_http_v2_state_ping, /* NGX_HTTP_V2_PING_FRAME */
ngx_http_v2_state_goaway, /* NGX_HTTP_V2_GOAWAY_FRAME */
ngx_http_v2_state_window_update, /* NGX_HTTP_V2_WINDOW_UPDATE_FRAME */
ngx_http_v2_state_continuation /* NGX_HTTP_V2_CONTINUATION_FRAME */
};

3. 请求头处理

H2 同样遵循 HTTP1.1 的语义, 需要先发送请求行、请求头, 不过在 H2 中将请求行信息转换成特殊的请求头. 对于 HTTP1.1 请求行是 Method SP Request-URI SP HTTP-Version CRLF 格式, 在 H2 中会将其拆分成 :method, :path 请求头.

请求头是通过 ngx_http_v2_state_headers, ngx_http_v2_state_header_block, ngx_http_v2_state_field_len(ngx_http_v2_state_field_huff/ngx_http_v2_state_field_raw), ngx_http_v2_state_process_header, ngx_http_v2_state_header_complete 函数处理.

  • ngx_http_v2_state_headers

    判断 StreamId 是否正确、请求头是否超限、流并发是否超限、分配 Stream 结构建立流依赖树.

  • ngx_http_v2_state_header_block

    请求头处理, 有 5 种类型的请求头(具体看 HPACK 协议, 不展开). 根据请求头长度进行处理.

  • ngx_http_v2_state_field_len(ngx_http_v2_state_field_huff/ngx_http_v2_state_field_raw)

    使用霍夫曼编码或原始编码解析请求头.

  • ngx_http_v2_state_process_header

    对解析出来的请求头进行处理: 校验是否合法, 将其保存到 r->headers_in.headers 中.

  • ngx_http_v2_state_header_complete

    判断是否有后续请求头, 进入 ngx_http_v2_state_header_block 继续处理. 如果设置标记 HEADERS 帧结束, 进入请求处理.

4. 请求处理

在 H2 请求头处理结束后会进入 ngx_http_v2_run_request 进行请求处理:

  1. 将 H2 格式请求信息转换成 HTTP1 格式信息(NGINX 内部使用, 能够复用 NGINX 原有功能);
  2. 进入 HTTP1 的请求处理函数 ngx_http_process_request, 调用 ngx_http_handler 运行 HTTP 处理的 11 个阶段.

在调用 ngx_http_v2_run_request 时使用的是 stream->request 作为参数, 是在 ngx_http_v2_state_headers 函数中创建的假的 request/connection, 读/写回调函数都是 ngx_http_v2_close_stream_handler.

ngx_http_process_request 中将请求读回调函数修改为 ngx_http_block_reading.

ngx_http_handler 中将请求写回调函数修改为 ngx_http_core_run_phases.

ngx_http_v2_read_request_body 中将读/写回调函数修改为 ngx_http_v2_read_client_request_body_handler/ngx_http_request_empty_handler.

5. 何时读取请求体?

前面已经提到在 ngx_http_handler 中会运行 HTTP 处理的 11 个阶段, 假设当前 location 用于反向代理那必定会运行 ngx_http_proxy_handler 函数. 可以追踪到函数调用链(注意, ngx_http_v2_read_client_request_body_handler 是通过回调触发):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
graph LR

subgraph HTTP 框架
ngx_http_handler --> ngx_http_core_run_phases
ngx_http_core_run_phases --> ...
end

subgraph Proxy
... --> ngx_http_proxy_handler
ngx_http_proxy_handler --> ngx_http_read_client_request_body
end

subgraph HTTP V2
ngx_http_read_client_request_body --> ngx_http_v2_read_request_body
ngx_http_v2_read_request_body -.-> ngx_http_v2_read_client_request_body_handler
end

看到这里是不是会想 ngx_http_v2_read_client_request_body_handler 负责请求体读取操作操作? 跟进函数去没有读取操作, 而且函数参数 r->connection 并没有与 socket 关联无法进行读写操作.

对于 H2 数据是通过 DATA 帧进行传输, 还是得从 ngx_http_v2_frame_states 状态机跟进. ngx_http_v2_state_data 用于处理 DATA 帧, 其中调用链如下:

1
2
3
4
graph LR
ngx_http_v2_state_data --> ngx_http_v2_state_read_data
ngx_http_v2_state_read_data --> ngx_http_v2_process_request_body
ngx_http_v2_process_request_body --> post_handler

此处 post_handler 就是 ngx_http_upstream_init, 是 ngx_http_proxy_handler 中设置.

6. 响应

NGINX 响应分为 HEADER、BODY 两个阶段, 看下源码有 ngx_http_v2_filter_module 模块, 模块只介入 header_filter 处理阶段, 在其中以 H2 格式发送响应头, 有两点需要注意:

  • 连接(connection)以及对应的套接字(socket):

    对于 H2 在 header_filter/body_filter 的入参 request 是“假”的, 其关联的 connection 也是假的. 需要使用 H2 初始建立的连接, 通过 r->stream->connection 索引.

  • 响应体处理:

    H2 模块没有添加 body_filter 处理函数, 在 header_filter 阶段修改 connectionsend_chain 回调函数为 ngx_http_v2_send_chain 用于 H2 响应体发送.

  • 响应 Stream Id:

    在响应阶段 request 是“假”的, 已经与请求 Stream 关联, 通过 r->stream->node->id 可以获得 sid.

这里只做了简略分析, 优先级、推送、窗口更新都没有提及.

四 抓包观察

1. 配置

NGINX 可以不基于 HTTPS 启用 H2, 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 8000 default_server http2;

location / {
content_by_lua_block {
ngx.log(ngx.ERR, "content phase")
local content = "Our examples focus on using server push to improve page load performance in web browsers."

for i = 1, 100 do
ngx.say(content)
ngx.sleep(0.1)
end
}
}
}

2. 请求

可以使用 CURL 发起 H2 请求:

1
2
3
4
5
6
7
curl -i --http2-prior-knowledge http://127.0.0.1:8000/
HTTP/2 200
server: openresty/1.15.8.2
date: Wed, 11 Aug 2021 01:17:25 GMT
content-type: text/plain

content phase

CURL 也支持发起 H3 请求, 方便抓包测试.

3. 抓包

客户端发送的 SETTINGS 帧信息

HTTP2 客户端发送 SETTINGS

服务端发送的 SETTINGS 帧信息

HTTP2 服务端发送 SETTINGS

注意, 服务端在响应 SETTINGS、WINDOW_UPDATE 帧时 Stream ID 都为 0; 在发送 HEADERS 帧 Stream ID 为 1, 是对客户端发起的 Stream ID 为 1 的响应. 即请求/响应在同一个流中进行.

五 参考文章